06.2 精通自定义 View 之 Paint 基本使用——文字

返回自定义 View 目录

6.2.1 概述

1. 四线格与基线

小时候,我们在刚开始学习写字母时,用的本子是四线格的,我们必须把字母按照规则写在四线格内。

在 canvas 在利用 drawText 绘制文字时,也是有规则的,这个规则就是基线!我们先来看一下什么是基线:

可见基线就是四线格中的第三条线。也就是说,只要基线的位置定了,那文字的位置必然是定了的!

2. canvas.drawText()

1)canvas.drawText() 与基线

1
2
3
4
5
6
7
/**
* text:要绘制的文字
* x:绘制原点x坐标
* y:绘制原点y坐标
* paint:用来做画的画笔
*/
public void drawText(String text, float x, float y, Paint paint)

上面这个构造函数是最常用的 drawText 方法,传进去一个 String 对象就能画出对应的文字。但这里有两个参数需要非常注意,表示原点坐标的 x 和 y。很多同学可能会认为,这里传进去的原点参数 (x,y) 是所在绘制文字所在矩形的左上角的点。但实际上并不是!比如,我们上面如果要画 “harvic’s blog” 这几个字,这个原点坐标应当是下图中绿色小点的位置。

一般而言,(x,y) 所代表的位置是所画图形对应的矩形的左上角点。但在 drawText 中是非常例外的,y 所代表的是基线的位置。

2)示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int baseLineX = 0;
int baseLineY = 200;
// 画基线
Paint paint = new Paint();
paint.setColor(Color.RED);
canvas.drawLine(baseLineX, baseLineY, 3000, baseLineY, paint);
// 写文字
paint.setColor(Color.GREEN);
paint.setTextSize(120); // 以px为单位
canvas.drawText("harvic\'s blog", baseLineX, baseLineY, paint);
}

首先,我们把 (0,200) 所在的这条横线画出来,所以我先画了一条线从点坐标为 (0,200) 到点坐标为 (3000,200) 的一条直线,然后利用 canvas.drawText 以 (0,200) 为原点画出文字,最终效果图如下:

结论:

  • drawText() 函数中的参数 y 是基线的位置。
  • 一定要清楚的是,只要 x 坐标、基线位置、文字大小确定,文字的位置就是确定了。

3. paint.setTextAlign() 函数

在上面我们讲了,drawText() 函数中的 y 参数表示所要绘制文字的基线所在位置。从上面的例子中可以看到,绘制是从 x 坐标的右边开始的,但这并不是必然的结果。我们来看一张图:

在 drawText(text, x, y, paint) 中传进去的原点坐标 (x,y)。其中,y 表示的基线的位置。那 x 代表什么呢?从上面的例子运行结果来看,应当是文字开始绘制的地方。

并不是!x 代表所要绘制文字所在矩形的相对位置。相对位置就是指指定点 (x,y) 在在所要绘制矩形的位置。我们知道所绘制矩形的纵坐标是由 y 值来确定的,而相对 x 坐标的位置,只有左、中、右三个位置了。也就是所绘制矩形可能是在 x 坐标的左侧绘制,也有可能在 x 坐标的中间,也有可能在 x 坐标的右侧。而定义在 x 坐标在所绘制矩形相对位置的函数是:

1
2
3
4
5
/**
* 其中 Align 的取值为:
* Panit.Align.LEFT, Paint.Align.CENTER, Paint.Align.RIGHT
*/
Paint::setTextAlign(Align align);

仍然使用上面的例子,当设置不同的 Align 取值时,效果如下图所示。
Paint.Align.LEFT:

Paint.Align.CENTER:

Paint.Align.RIGHT:

4. 注意

这里需要再次强调的是:相对位置是根据所要绘制文字所在矩形来计算的。比如,只写一个大写字母 A,将其相对位置设置为 Paint.Align.CENTER。

1
2
mPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText("A", baseLineX, baseLineY, mPaint);

效果如下图所示:

6.2.2 绘图四线格与 FontMetrics

1. 文字的绘图四线格

除了基线以外,系统在绘制文字时还有 4 条线,分别是 ascent、descent、top、bottom,如下图所示。

  • ascent:系统建议的,绘制单个字符时,字符应当的最高高度所在线。
  • descent:系统建议的,绘制单个字符时,字符应当的最低高度所在线。
  • top:可绘制的最高高度所在线。
  • bottom:可绘制的最低高度所在线。

我们在绘制文字时,ascent 是推荐的绘制文字的最高高度,就表示在绘制文字时,尽力要在这个最高高度以下绘制文字。descent 是推荐的绘制文字的最底高度线,同样表示是在绘制文字时尽量在这个 descent 线以上来绘制文字。而 top 线则指该文字可以绘制的最高高度线,bottom 则是表示该文字可以绘制的最低高度线。ascent、descent 是系统建议上的绘制高度,而 top、bottom 则是物理上屏幕最高、最低可以画的高度值。

2. FontMetrics

1)FontMetrics 概述
我们知道基线的位置是我们在构造 drawText() 函数时由参数 y 来决定的,那 ascent、descent、top、bottom 这些线的位置要怎么计算出来呢?

Android 给我们提供了一个类:FontMetrics,它里面有四个成员变量:

1
2
3
4
FontMetrics::ascent;
FontMetrics::descent;
FontMetrics::top;
FontMetrics::bottom;

他们的意义与值的计算方法分别如下:

  • ascent = ascent 线的 y 坐标 - baseline 线的 y 坐标。
  • descent = descent 线的 y 坐标 - baseline 线的 y 坐标。
  • top = top 线的 y 坐标 - baseline 线的 y 坐标。
  • bottom = bottom 线的 y 坐标 - baseline 线的 y 坐标。

我们再来看个图:

从这个图中,我们先说明两点,然后再回过头来看上面的公式:
1、X 轴,Y 轴的正方向走向是 X 轴向右是正方向,Y 轴向下是正方向,所以越往下 Y 坐标越大!
2、大家千万不要将 FontMetrics 中的 ascent、descent、top、bottom 与现实中的 ascent、descent、top、bottom 所在线混淆!这几条线是真实存在的,而 FontMetrics 中的 ascent、descent、top、bottom 这个变量的值就是用来计算这几条线的位置的。

1
ascent = ascent 线的 y 坐标 - baseline 线的 y 坐标

FontMetrics 的这几个变量的值都是以 baseline 为基准的,对于 ascent 来说,baseline 线在 ascent 线之下,所以必然 baseline 的 y 值要大于 ascent 线的 y 值,所以 ascent 变量的值是负的。

同理,对于 descent 而言:

1
descent = descent 线的 y 坐标 - baseline 线的 y 坐标

descent 线在 baseline 线之下,所以必然 descent 线的 y 坐标要大于 baseline 线的 y 坐标,所以 descent 变量的值必然是正数。

2)得到 Text 四线格的各线位置
先列出一个公式:

1
ascent 线 Y 坐标 = baseline 线 Y 坐标 + fontMetric.ascent

推算过程如下:

因为 ascent 线的 Y 坐标等于 baseline 线的 Y 坐标减去从 baseline 线到 ascent 线的这段距离。也就是:(|fontMetric.ascent| 表示取绝对值)。
ascent 线 Y 坐标 = baseline 线 Y 坐标 - |fontMetric.ascent|;
又因为 fontMetric.ascent 是负值,所以:
ascent 线 Y 坐标 = baseline 线 Y 坐标 - |fontMetric.ascent|;
ascent 线 Y 坐标 = baseline 线 Y 坐标 - (-fontMetric.ascent);
ascent 线 Y 坐标 = baseline 线 Y 坐标 + fontMetric.ascent;

这就是整个推算过程,没什么难度,同理可以得到:

  • ascent 线 Y 坐标 = baseline 线的 y 坐标 + fontMetric.ascent;
  • descent 线 Y 坐标 = baseline 线的 y 坐标 + fontMetric.descent;
  • top 线 Y 坐标 = baseline 线的 y 坐标 + fontMetric.top;
  • bottom 线 Y 坐标 = baseline 线的 y 坐标 + fontMetric.bottom;

3)获取 FontMetrics 对象

1
2
3
Paint paint = new Paint();
Paint.FontMetrics fm = paint.getFontMetrics();
Paint.FontMetricsInt fmInt = paint.getFontMetricsInt();

从这里可以看到,通过 paint.getFontMetrics() 得到对应的 FontMetrics 对象。这里还有另外一个 FontMetrics 同样的类叫做 FontMetricsInt,它的意义与 FontMetrics 完全相同,只是得到的值的类型不一样而已,FontMetricsInt 中的四个成员变量的值都是 Int 类型,而 FontMetrics 得到的四个成员变量的值则都是 float 类型的。

4)示例:计算 Text 四线格位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class TestView extends View {
private Paint mPaint;
private Paint.FontMetrics mFm;
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.GREEN);
mPaint.setTextSize(120);
mPaint.setTextAlign(Paint.Align.LEFT);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int baseLineX = 0;
int baseLineY = 200;
// 写文字
canvas.drawText("harvic\'s blog", baseLineX, baseLineY, mPaint);
// 计算各线在位置
mFm = mPaint.getFontMetrics();
float ascent = baseLineY + mFm.ascent;
float descent = baseLineY + mFm.descent;
float top = baseLineY + mFm.top;
float bottom = baseLineY + mFm.bottom;
// 画基线
mPaint.setColor(Color.RED);
canvas.drawLine(baseLineX, baseLineY, 3000, baseLineY, mPaint);
// 画 top
mPaint.setColor(Color.BLUE);
canvas.drawLine(baseLineX, top, 3000, top, mPaint);
// 画 ascent
mPaint.setColor(Color.GREEN);
canvas.drawLine(baseLineX, ascent, 3000, ascent, mPaint);
// 画 descent
mPaint.setColor(Color.YELLOW);
canvas.drawLine(baseLineX, descent, 3000, descent, mPaint);
// 画 bottom
mPaint.setColor(Color.RED);
canvas.drawLine(baseLineX, bottom, 3000, bottom, mPaint);
}
}

6.2.3 常用函数

1. 字符串所占高度和宽度

1)高度
字符串所占高度很容易得到,直接用 bottom 线所在位置的 Y 坐标减去 top 线所在位置的 Y 坐标就是字符串所占的高度:

1
2
3
4
5
mFm = mPaint.getFontMetricsInt();
int top = baseLineY + fm.top;
int bottom = baseLineY + fm.bottom;
// 所占高度
int height = bottom - top;

2)、宽度
宽度是非常容易得到的,直接利用下面的函数就可以得到:

1
int width = mPaint.measureText("harvic's blog");

2. 最小矩形

1)概述
要获取最小矩形,也是通过系统函数来获取的,函数及意义如下:

1
2
3
4
5
6
7
8
/**
* 获取指定字符串所对应的最小矩形,以(0,0)点所在位置为基线
* @param text 要测量最小矩形的字符串
* @param start 要测量起始字符在字符串中的索引
* @param end 所要测量的字符的长度
* @param bounds 接收测量结果
*/
public void getTextBounds(String text, int start, int end, Rect bounds);

示例:

1
2
3
4
5
6
7
8
String text = "harvic\'s blog";
Paint paint = new Paint();
// 设置paint
paint.setTextSize(120);
Rect minRect = new Rect();
paint.getTextBounds(text,0,text.length(),minRect);
Log.e("xian",minRect.toShortString());

可以看到这个矩形的左上角位置为(8,-90),右下角的位置为(654,25);大家可能会有疑问,为什么左上角的 Y 坐标是个负数?从代码中,我们也可以看到,我们并没有给 getTextBounds() 传递基线位置。那它就是以(0,0)为基线来得到这个最小矩形的,所以这个最小矩形的位置就是以(0,0)为基线的结果。

2)得到最小矩形的实际位置
我们先来看一个原理图:

在上面这个图中,我们将黑色矩形平行下移距离Y(黄色线依照的是基线的位置),那么平移后的左上角点的 y 坐标就是 y2 = y1 + Y。

同样的道理,由于 paint.getTextBounds() 得到最小矩形的基线是 y = 0;那我们直接将这个矩形移动 baseline 的距离就可以得到这个矩形实际应当在的位置了。

所以矩形应当所在实际位置的坐标是:

1
2
3
4
5
6
Rect minRect = new Rect();
paint.getTextBounds(text,0,text.length(),minRect);
// 最小矩形,实际 top 位置
int minTop = bounds.top + baselineY;
// 最小矩形,实际 bottom 位置
int minBottom = bounds.bottom + baselineY;

3)完整的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String text = "harvic\'s blog";
int baseLineY = 200;
int baseLineX = 0 ;
// 设置paint
Paint paint = new Paint();
paint.setTextSize(120); //以px为单位
paint.setTextAlign(Paint.Align.LEFT);
// 画text所占的区域
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int top = baseLineY + fm.top;
int bottom = baseLineY + fm.bottom;
int width = (int)paint.measureText(text);
Rect rect = new Rect(baseLineX,top,baseLineX+width,bottom);
paint.setColor(Color.GREEN);
canvas.drawRect(rect,paint);
// 画最小矩形
Rect minRect = new Rect();
paint.getTextBounds(text,0,text.length(),minRect);
minRect.top = baseLineY + minRect.top;
minRect.bottom = baseLineY + minRect.bottom;
paint.setColor(Color.RED);
canvas.drawRect(minRect,paint);
// 写文字
paint.setColor(Color.BLACK);
canvas.drawText(text, baseLineX, baseLineY, paint);
}

6.2.4 示例:定点写字

1. 给定左上顶点绘图

在这个图中,我们给定左上角的位置,即 (left,top);我们知道要画文字,drawText() 中传进去的 Y 坐标是基线的位置,所以我们就必须根据 top 的位置计算出 baseline 的位置。

我们来看一个公式:
FontMetrics.top = top - baseline;

所以:
baseline = top - FontMetrics.top;

因为 FontMetrics.top 是可以得到的,又因为我们的 top 坐标是给定的,所以通过这个公式就能得到 baseline 的位置了。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class TestView extends View {
private Paint mPaint;
private Paint.FontMetrics mFm;
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.GREEN);
mPaint.setTextSize(120);
mPaint.setTextAlign(Paint.Align.LEFT);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mFm = mPaint.getFontMetrics();
float top = 0;
int baseLineX = 0;
float baseLineY = top - mFm.top;
// 写文字
canvas.drawText("harvic\'s blog", 0, baseLineY, mPaint);
// 画基线
mPaint.setColor(Color.RED);
canvas.drawLine(baseLineX, baseLineY, 3000, baseLineY, mPaint);
}
}

2. 给定中间线位置绘图

先来看一张图:

在这个图中,总共有四条线:top 线、bottom 线、baseline 和 center线。其中 center 线正是在 top 线和 bottom 线的正中间。

为了方便推导公式,另外标了三个距离 A、B、C。显然,距离 A 和距离 C 是相等的,都等于文字所在矩形高度以的一半,即 A = C = (bottom - top)/2。

又因为:
bottom = baseline + FontMetrics.bottom
top = baseline + FontMetrics.top
将这两个公式代入上面的公式,就可得到:
A = C = (FontMetrics.bottom - FontMetrics.top)/2

而距离 B 则表示 center 线到 baseline 的距离。很显然距离
B = C - (bottom - baseline)

又因为:
FontMetrics.bottom = bottom - baseline
C = A
所以:
B = A - FontMetrics.bottom

从而有:
baseline = center + B = center + A - FontMetrics.bottom = center + (FontMetrics.bottom - FontMetrics.top)/2 - FontMetrics.bottom

根据上面的推导过程,我们最终可知,当给定中间线 center 位置以后,baseline 的位置为:
baseline = center + (FontMetrics.bottom - FontMetrics.top)/2 - FontMetrics.bottom